<template>
    <div ref="refSelf" class="full column" style="padding:30px;">
        
        <div class="row shrink0">
    
            <h1 class="grow noselect" @click="onClickTitle">固件下载</h1>
            
            <n-button @click="bus.emit('topview.enter()', 'index')" title="返回">返回</n-button>

        </div>

        <div class="shrink0" style="margin-top:20px">
            下载、升级ESP设备上的固件。请使用 USB 数据线缆将您的设备连接到 PC 上。
        </div>

        <!-- <div class="row form-row item-center shrink0">
            <span>固件</span>
            <n-space>
                <n-select v-model:value="selectedFirmwareIndex" style="width:300px" :options="lstFirmwares"></n-select>

                <n-button @click="refreshFirmwareList">刷新</n-button>
                <n-button @click="openFirmwareDir">打开目录</n-button>
            </n-space>
        </div> -->


        <!-- <div class="row form-row item-center shrink0">
            <span style="width:80px">分区方案</span>
            <n-space>
                <n-select v-model:value="selectedPartitionSln" style="width:300px" :options="PartitionSlns" @update:value="onPartitionSlnChanged"></n-select>
            </n-space>
        </div> -->
        
        <div class="row form-row item-center shrink0">
            <span style="width:80px">设备</span>
            
            <n-space>
                <n-select v-model:value="seletedDevicePath" style="width:300px" :options="lstUsbDevices" @update:value="onDeviceChanged"></n-select>
                <n-button @click="refreshUsbDevices" :disabled="bRefreshingUsb">刷新</n-button>
            </n-space>
        </div>

        <div class="row form-row item-center shrink0">
            <span style="width:80px"></span>
            <n-space>
                {{ chipType }}
                {{ flashSize }}

                <n-button text style="margin-right:15px" @click="onDetectFlashSize">
                    Detect
                </n-button>
            </n-space>
        </div>

        <div class="row form-row item-center shrink0">
            <n-button type="primary"
                    style="margin-right: 10px;"
                    :disabled="seletedDevicePath==null || bBurnning"
                    @click="runEraseFirmware"
                    :loading="!!bErasing">
                        {{bErasing? '正在擦除固件 ...': '擦除'}}
            </n-button>

            <n-button type="primary"
                    style="margin-right: 10px;"
                    :disabled="selectedFirmwareIndex<0 || seletedDevicePath==null || bErasing"
                    @click="burnSelectedFirmware()"
                    :loading="!!bBurnning">
                        {{bBurnning? '正在烧录固件 ...': '开始烧录固件'}}
            </n-button>
            <n-button v-show="esptoolsProcess" @click="stopBurn" type="error" style="margin-right: 10px;">停止</n-button>
            {{esptoolsProcess}}
            <!-- <n-button @click="showAdvance=!showAdvance" type="text">高级</n-button> -->
            <!-- <n-button @click="onDetectFlashSize" type="text">FLASH</n-button> -->

            <div class="grow"></div>
            <n-button @click="cleanOutput" type="text">清除</n-button>

            <n-button @click="bus.emit('terminal.popup()')" type="text" style="margin-left:0px">终端</n-button>
        </div>
        
        <div class="grow row gap20" style="margin-top: 20px; margin-left: 20px;">

            <div v-show="showAdvance" class="column shrink0" style="width: 550px;">

                <div class="row gap10 item-center" style="">
                    <n-checkbox v-model:checked="bAutoBurn" @update:checked="saveFileList">自动烧录(当文件变化时)</n-checkbox>
                    <div class="grow"></div>
                    <n-button @click="addFile()">+</n-button>
                    <n-button @click="checkFileAll(true)">全选</n-button>
                    <n-button @click="checkFileAll(false)">全取消</n-button>
                    <n-button @click="invFileAll()">反选</n-button>
                </div>

                <div style="min-width:600px; overflow-y: scroll; margin-top:20px" class="grow column gap10">
                    <div v-for="binfile, fileidx in filelist"
                                style="padding:5px 5px 5px 5px;" 
                                @mouseover="binfile.hover=true" @mouseleave="binfile.hover=false" :style="{ backgroundColor: binfile.hover? '#efefef': '#fff' }"
                                @ondrop="dropFile(event, binfile)">
                        <div class="row gap5 item-center">
                            <n-checkbox class="shrink0" v-model:checked="binfile.selected" @update:checked="saveFileList"></n-checkbox>

                            @<n-input style="width:120px" v-model:value="binfile.offset" :show-button="false" placeholder="分区地址" @update:value="saveFileList" />
                            
                            <n-input-group class="grow">
                                <n-input v-model:value="binfile.path" placeholder="文件本地路径"/>
                                <n-button class="shrink0" @click="browseFile(binfile)">浏览</n-button>
                            </n-input-group>

                            <div style="color:#d0d0d0; margin-right:20px" class="shrink0">{{ binfile.modifytime? (new Date(binfile.modifytime+28800000)).toISOString().substr(11,8): '--'}}</div>

                            <n-button text
                                @click="burnSelectedFirmware([binfile], false)"
                                :title="binfile.path">
                                烧录
                            </n-button>
                            <n-button text style="margin-right:15px" @click="removeFile(fileidx)"> 移除 </n-button>
                        </div>
                        <div style="color:#d0d0d0; margin-top:10px" class="shrink0">{{binfile.desc}}</div>
                    </div>
                </div>
            </div>

            <audio ref="refAudioDone" src="media/task-done-1.mp3"></audio>

            <div ref="refOutput" class="grow" style="font-family: monospace; padding:20px; overflow-y: scroll; margin-left: 30px;"></div>

        </div>

        <!-- <Factory></Factory> -->

    </div>

</template>

<style scoped>
.form-row {
    margin-top: 20px;
}
.form-row span {
    width: 50px ;
}
.form-row div {
    flex-grow: 1;
}
</style>

<script lang="ts" setup>
import { ref, onMounted, reactive, Ref, watch } from 'vue'
import {bus} from '@beshell/events-ts/bus'
import {readdir, exists, readFile} from  "../../base/fs.async"
import ChromeSerialSocket from '@beshell/conn-ts/ChromeSerialSocket'
import { conn } from '@beshell/conn-ts/Connection'
import SerialSocket from '@beshell/conn-ts/SerialSocket'
import { getDocumentsDir, sleep } from '../../base/utils'
// import Factory from './Factory.vue'
import {isDebug} from '../../issue'
import {openFirmwareDir, fileModifyTime, loadFirmwares, detectFlashSize, burnFirmware, eraseFirmware, runPython, runEspToolProcess} from './download'
import { createAutoTimer } from '@/base/autotimer'
const child_process = require("child_process")
const os = require('os');
const fs = require('fs');

interface Window {
  showOpenFilePicker(options?: any): Promise<FileSystemFileHandle[]>;
}


if(window.runtime.platform=="nw.js") {
    var path: any = (window as any).require("path")
    var statSync: any = (window as any).require("fs").statSync
    var gui = (window as any).require('nw.gui')
    var { spawn, execSync } = (window as any).require('child_process')
}


const lstFirmwares: any[] = reactive([])
const selectedFirmwareIndex = ref(-1)

const bRefreshingUsb = ref(false)
const lstUsbDevices: any[] = reactive([])
const seletedDevicePath = ref(null)


const bErasing = ref(false)
const bBurnning = ref(false)
const esptoolsProcess: any = ref(null)
const bAutoBurn = ref(false)

let bOutputAutoScroll = true

const showAdvance = ref(true)

let flashSize: Ref<number> = ref(0)
let chipType: Ref<string> = ref("")
let chip: string|null = null

let $self: any
const refSelf = ref(null) as unknown as Ref<HTMLElement>
const refOutput = ref(null) as unknown as Ref<HTMLElement>
const refAudioDone = ref(null) as unknown as Ref<HTMLAudioElement>

const PartitionSlns = [
    {
        label: "ESP32 S3 4MB/8MB/16MB" ,
        value: 0 ,
        files: [
            {
                selected: true ,
                hover: false ,
                offset: "0x8000" ,
                path: "" ,
                modifytime:0 ,
                desc: "分区表",
            } ,
            {
                selected: true ,
                hover: false ,
                offset: "0x10000" ,
                path: "" ,
                modifytime:0 ,
                desc: "native固件",
            } ,
            {
                selected: true ,
                hover: false ,
                offset: "0x8D0000" ,
                path: "" ,
                modifytime:0 ,
                desc: "JavaScript固件",
            } ,
        ]
    }
]
const selectedPartitionSln = ref(-1)

const filelist: any = ref([])


onMounted (()=>{
    
    bus.on('firmware.popup()', ()=>{
        popup()
    })
    bus.on('firmware.popdown()', ()=>{
        popdown()
    })
    
    $self = $(refSelf.value)

    // $self.appendTo(document.body)
    // $self.css('top',$(window).height())

    // rootdir = path.dirname((window as any).process.mainModule.filename)
    // firmdir = rootdir + path.sep + "assets" + path.sep + "firmware"
    
    // firmdir = path.format(path.parse(firmdir))

    refreshFirmwareList()

    refreshUsbDevices()

    loadFileList()
})

async function refreshFirmwareList() {
    // 清空
    lstFirmwares.length = 0

    lstFirmwares.push(... await loadFirmwares(selectedFirmwareIndex))
}

function onDeviceChanged() {
    console.log(onDeviceChanged)
    flashSize.value = 0
    chipType.value = ''
    chip = null
}


async function refreshUsbDevices() {

    flashSize.value = 0
    chipType.value = ''
    chip = null

    bRefreshingUsb.value = true
    let oldSelectedPath = seletedDevicePath.value
    seletedDevicePath.value = null

    // 清空
    lstUsbDevices.length = 0

    // 加载                
    for(let dev of await ChromeSerialSocket.getDevices()) {
        console.log(dev)
        lstUsbDevices.push({
            label: `${dev.path.replace(/^[\\\.]+/,'')} (${dev.displayName.substr(0,20)} ${dev.displayName.length>20?'...':''})`,
            value: dev.path,
        })

        if(dev.path == oldSelectedPath) {
            seletedDevicePath.value = oldSelectedPath
        }
    }

    if(!seletedDevicePath.value && lstUsbDevices.length)
        seletedDevicePath.value = lstUsbDevices[0].value
        
    bRefreshingUsb.value = false
}



async function autoClosePort(devicePath:string|null) {
    if(conn.isConnected && (conn.active as SerialSocket)?.path == devicePath) {
        output("关闭设备串口连接： "+devicePath+"\r\n\r\n")
        bus.emit("terminal.echo()", "关闭设备串口连接： "+devicePath)
        await conn.active?.close()
        return true
    }
    else
        return false
}


async function burnSelectedFirmware(binlst: any=null, useEnable=true) {

    if(bBurnning.value || bErasing.value) {
        return
    }

    let t0 = Date.now()

    bBurnning.value = true

    try {

        let devicePath = seletedDevicePath.value as unknown as string

        let reconnect = await autoClosePort(devicePath)

        if(!flashSize || !chipType.value) {
            output("查询芯片FLASH尺寸...\r\n")
            ; [chipType.value, chip, flashSize.value] = await detectFlashSize(devicePath.replace(/^[\\\.]+/,''), output)

            console.log(chipType.value, chip, flashSize.value)

            if(!flashSize.value){
                output("无法取得芯片FLASH尺寸信息。\r\n\r\n")
                bBurnning.value = false
                return
            }
            if(!chipType.value || chipType.value=="Unsupported"){
                output("无法取得芯片类型。\r\n\r\n")
                bBurnning.value = false
                return
            }
        }

        output(`\r\n\r\nchip: ${chip} [${chipType.value}] ; flash size: ${flashSize.value}\r\n`)

        // let flashSize = "4"
        // let firmjson: any = lstFirmwares[selectedFirmwareIndex.value]
        
        // console.log(firmjson, flashSize, filelst)     
        if(!binlst) {
            binlst = [ ... (filelist.value) ]
        }
        if(useEnable) {
            binlst = binlst.filter((bin: any)=>bin.selected)
        }

        output("\r\n\r\n开始烧录固件 ...\r\n")
        let promise = burnFirmware(devicePath.replace(/^[\\\.]+/,''), binlst, output)
        esptoolsProcess.value = (promise as any).process
        
        await promise

        output( "\r\n用时:" + Math.round((Date.now()-t0)/1000)+" sec\r\n\r\n" )

        await sleep(100)
        
        if(reconnect && !conn.isConnected) {
            output("重新连接到串口设备： "+devicePath.replace(/^[\\\.]+/,'')+"\r\n\r\n")
            bus.emit("terminal.echo()", "重新连接到串口设备： "+devicePath.replace(/^[\\\.]+/,'')+"\r\n")
            await conn.active?.connect()
        }
        
        await sleep(500)
        output(createAutoTimer(new Date(), "div", refOutput?.value))

    }catch(e:any){
        console.error(e)
        output(e.toString())
    }

    playAudioDone()

    bBurnning.value = false
}


// async function testPython() {
//     await runEspToolProcess([], output, "test.py")
// }

// window.testPython = testPython

async function runEraseFirmware() {

    bErasing.value = true

    let devicePath = seletedDevicePath.value as unknown as string

    let reconnect = await autoClosePort(devicePath)

    await eraseFirmware(devicePath, output)

    if(reconnect && !conn.isConnected) {
        output("重新连接到串口设备： "+devicePath+"\r\n\r\n")
        bus.emit("terminal.echo()", "重新连接到串口设备： "+devicePath+"\r\n")
        await conn.active?.connect()
    }
    
    bErasing.value = false
}

function stopBurn() {
    if(!esptoolsProcess.value){
        return
    }
    output("\r\n终止操作")

    if(os.platform() === 'win32'){
        child_process.exec('taskkill /pid ' + esptoolsProcess.value.pid + ' /T /F')
    }
    else {
        esptoolsProcess.value.kill()
    }
}

function output(content: string|HTMLElement) {
    console.log("output>", content)

    if(typeof content=="string") {
        refOutput.value.innerText+= content
    }
    else if(content instanceof HTMLElement) {
        refOutput.value.appendChild(content)
    }

    if(bOutputAutoScroll){
        refOutput.value.scrollTop = refOutput.value.scrollHeight
    }
}

function cleanOutput() {
    refOutput.value.innerText = ""
}



function popup() {
    // bus.emit('factory.popup()')
    return new Promise(resolve=>{
        $self.show().animate({
            top: 0
        }, 200, "swing", resolve)
    })
}
function popdown() {
    return new Promise<void>(resolve=>{
        let height = $(document.body).height()
        $self.animate({
            top: height
        }, 200, "swing", ()=>{
            $self.hide()
            resolve()
        })
    })
}

// TitleClickMS 秒内点击 TitleClickTimes 次进入工厂模式
const TitleClickTimes = isDebug? 1: 5
const TitleClickMS = 2000
let titleClickRecord: number[] = []

function onClickTitle() {
    let now = Date.now()
    titleClickRecord.push(now)

    while(titleClickRecord.length>TitleClickTimes) {
        titleClickRecord.shift()
    }
    if(titleClickRecord.length==TitleClickTimes) {
        if( now - titleClickRecord[0] < TitleClickMS ) {
            bus.emit("factory.popup()")
        }
    }
}


function checkFileAll(checked:boolean) {
    if(selectedFirmwareIndex.value==-1 || !lstFirmwares[selectedFirmwareIndex.value])
        return 
    let firmjson: any = lstFirmwares[selectedFirmwareIndex.value]
    for(let bininfo of firmjson.bin||[]) {
        bininfo.enable = checked
    }
}


function invFileAll() {
    if(selectedFirmwareIndex.value==-1 || !lstFirmwares[selectedFirmwareIndex.value])
        return 
    let firmjson: any = lstFirmwares[selectedFirmwareIndex.value]
    for(let bininfo of firmjson.bin||[]) {
        bininfo.enable = !bininfo.enable
    }
}

setInterval(()=>{
    if( bBurnning.value || bErasing.value) {
        return
    }

    let modifyBins: any = []
    for(let bininfo of filelist.value) {
        if( !(bininfo.modifytime>1) ) {
            continue
        }
        let mtime = fileModifyTime(bininfo.path)
        // console.log(bininfo.modifytime, mtime, bininfo.path)
        if(mtime>bininfo.modifytime) {
            bininfo.modifytime = mtime
            output("bin files has changed: " + bininfo.path + "\r\n")
            if(bininfo.selected && bAutoBurn.value) {
                modifyBins.push(bininfo)
            }
        }
    }

    if( bAutoBurn.value && modifyBins.length ) {
        output(modifyBins.length + " bin files has changed, auto burnning ...\r\n")
        playAudioStart()
        burnSelectedFirmware(modifyBins, true)
    }

}, 1000) 

function playAudioStart() {
    refAudioDone.value.src = "media/task-start-1.mp3"
    refAudioDone.value.play()
}
function playAudioDone() {
    refAudioDone.value.src = "media/task-done-1.mp3"
    refAudioDone.value.play()
}
async function onDetectFlashSize() {
    let devicePath = seletedDevicePath.value as unknown as string
    ; [chipType.value, chip, flashSize.value] = await detectFlashSize(devicePath, output)
}

function addFile() {
    filelist.value.push({
        selected: false,
        hover: false,
        offset: 0x00,
        path: "",
        modifytime:0,
    })
    saveFileList()
}

function removeFile(index) {
    filelist.value.splice(index, 1)
    saveFileList()
}
async function browseFile(fileItem) {
    const input = document.createElement('input')
    input.type = 'file'
    input.onchange = (event:any) => {
        const file = event.target.files[0]
        if (file) {
            const filePath = file.path
            console.log('Selected file path:', filePath)
            fileItem.path = filePath
            fileItem.modifytime = fileModifyTime(filePath)

            saveFileList()
        }
    }

    input.click()
}
function dropFile(event, fileItem) {
    console.log(event, fileItem)
}

function onPartitionSlnChanged(index:number) {
    if(!PartitionSlns[index]) {
        return
    }
    selectedPartitionSln.value = index
    filelist.value.length = 0
    for(let files of PartitionSlns[index].files) {
        console.log(files)
        filelist.value.push({
            ...files
        })
    }
}

function parseAddr(value:string) {
    return parseInt(value)
}
function formatAddr(value:number) {
    return "0x"+value.toString(16)
}

function saveFileList() {
    fs.writeFile(getDocumentsDir() + "/firmware-filelist.json", JSON.stringify(filelist.value, null, 4), (err:any) => {
        if (err) {
            console.error(err)
            return
        }
        console.log('The file has been saved!')
    })

    fs.writeFile(getDocumentsDir() + "/firmware-config.json", JSON.stringify({autoBurn: bAutoBurn.value}, null, 4), (err:any) => {
        if (err) {
            console.error(err)
            return
        }
        console.log('The file has been saved!')
    })
}

function loadFileList() {
    try {
        let jsonpath = getDocumentsDir() + "/firmware-filelist.json"
        let jsonstr = fs.readFileSync(jsonpath, 'utf8')
        let jsonobj = JSON.parse(jsonstr)
        filelist.value = jsonobj
    } catch (error) {
        console.error(error)
    }

    
    try {
        let jsonpath = getDocumentsDir() + "/firmware-config.json"
        let jsonstr = fs.readFileSync(jsonpath, 'utf8')
        let jsonobj = JSON.parse(jsonstr)
        bAutoBurn.value = jsonobj.autoBurn
    } catch (error) {
        console.error(error)
    }
}


// window.output = output
// window.createAutoTimer = createAutoTimer

</script>